class SISTimelineTool { static get toolbox() { return { title: 'Timeline Group', icon: '' }; } constructor({ data, api, readOnly }) { this.api = api; this.readOnly = readOnly; this.data = { globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '', items: (data && data.items && Array.isArray(data.items)) ? data.items : [] }; // Add an initial empty item if list is empty if (this.data.items.length === 0) { this.data.items.push({ date: '', description: '', imageUrl: '' }); } this.wrapper = undefined; } render() { this.wrapper = document.createElement('div'); this.wrapper.className = 'timeline-tool-wrapper'; this.wrapper.style.border = '1px solid #e3e6f0'; this.wrapper.style.borderRadius = '8px'; this.wrapper.style.padding = '14px'; this.wrapper.style.background = '#f8f9fc'; this.wrapper.style.marginBottom = '12px'; const headerLabel = document.createElement('label'); headerLabel.className = 'font-weight-bold text-primary small mb-2 d-block'; headerLabel.innerHTML = ' Timeline Group Settings'; this.wrapper.appendChild(headerLabel); const globalAttrGroup = this._createInput('e.g. data-aos="fade-up" style="..."', this.data.globalAttributes, 'global-attr', ' Global Custom Attributes'); this.wrapper.appendChild(globalAttrGroup); this.itemsContainer = document.createElement('div'); this.wrapper.appendChild(this.itemsContainer); this.data.items.forEach((item, index) => { this._renderItem(item, index); }); if (!this.readOnly) { const addButton = document.createElement('button'); addButton.type = 'button'; addButton.className = 'btn btn-sm btn-outline-primary mt-2'; addButton.innerHTML = ' Add Event'; addButton.addEventListener('click', () => { const newItem = { date: '', description: '', imageUrl: '' }; this.data.items.push(newItem); this._renderItem(newItem, this.data.items.length - 1); }); this.wrapper.appendChild(addButton); } return this.wrapper; } _renderItem(item, index) { const itemBox = document.createElement('div'); itemBox.className = 'timeline-item-edit-box mb-3 p-3 bg-white border rounded'; itemBox.style.position = 'relative'; if (!this.readOnly) { const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'btn btn-sm btn-danger'; removeBtn.style.position = 'absolute'; removeBtn.style.top = '10px'; removeBtn.style.right = '10px'; removeBtn.innerHTML = ''; removeBtn.addEventListener('click', () => { itemBox.remove(); }); itemBox.appendChild(removeBtn); } const dateGroup = this._createInput('Date (e.g. 10/04/1994)', item.date, 'date', ' Date / Milestone'); const descGroup = this._createTextarea('Description (e.g. Bệnh viện khai trương)', item.description, 'desc', ' Description'); // Image input with preview, upload, and media library buttons const imageGroup = document.createElement('div'); imageGroup.className = 'form-group mb-2'; const imageLabel = document.createElement('label'); imageLabel.className = 'small font-weight-bold text-secondary mb-1 d-block'; imageLabel.innerHTML = ' Image URL (Optional)'; const inputRow = document.createElement('div'); inputRow.className = 'input-group input-group-sm mb-2'; const imageInput = document.createElement('input'); imageInput.type = 'text'; imageInput.className = 'form-control ce-timeline-input-image'; imageInput.placeholder = 'Paste image URL here...'; imageInput.value = item.imageUrl || ''; if (this.readOnly) imageInput.disabled = true; const appendDiv = document.createElement('div'); appendDiv.className = 'input-group-append'; const chooseBtn = document.createElement('button'); chooseBtn.type = 'button'; chooseBtn.className = 'btn btn-info'; chooseBtn.innerHTML = ' Choose'; if (this.readOnly) chooseBtn.disabled = true; const uploadBtn = document.createElement('button'); uploadBtn.type = 'button'; uploadBtn.className = 'btn btn-primary'; uploadBtn.innerHTML = ' Upload'; if (this.readOnly) uploadBtn.disabled = true; // Hidden file input for uploading const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; chooseBtn.addEventListener('click', () => { if (window.SISMediaPicker) { SISMediaPicker.open((url, mediaObj, config) => { imageInput.value = url; // Store config in hidden inputs on the item box let styleInp = itemBox.querySelector('.ce-timeline-input-imgstyle'); if (!styleInp) { styleInp = document.createElement('input'); styleInp.type = 'hidden'; styleInp.className = 'ce-timeline-input-imgstyle'; itemBox.appendChild(styleInp); } let classInp = itemBox.querySelector('.ce-timeline-input-imgclass'); if (!classInp) { classInp = document.createElement('input'); classInp.type = 'hidden'; classInp.className = 'ce-timeline-input-imgclass'; itemBox.appendChild(classInp); } let altInp = itemBox.querySelector('.ce-timeline-input-imgalt'); if (!altInp) { altInp = document.createElement('input'); altInp.type = 'hidden'; altInp.className = 'ce-timeline-input-imgalt'; itemBox.appendChild(altInp); } let attrsInp = itemBox.querySelector('.ce-timeline-input-imgattrs'); if (!attrsInp) { attrsInp = document.createElement('input'); attrsInp.type = 'hidden'; attrsInp.className = 'ce-timeline-input-imgattrs'; itemBox.appendChild(attrsInp); } let arInp = itemBox.querySelector('.ce-timeline-input-imgar'); if (!arInp) { arInp = document.createElement('input'); arInp.type = 'hidden'; arInp.className = 'ce-timeline-input-imgar'; itemBox.appendChild(arInp); } if (config) { styleInp.value = config.style || ''; classInp.value = config.cssClass || ''; altInp.value = config.alt || ''; attrsInp.value = config.customAttributes || ''; arInp.value = config.aspectRatio || ''; } updatePreview(); }); } else { alert('SISMediaPicker is not loaded.'); } }); uploadBtn.addEventListener('click', () => { fileInput.click(); }); const updatePreview = () => { const url = imageInput.value.trim(); if (url) { imagePreview.style.backgroundImage = 'url("' + url + '")'; imagePreview.style.display = 'block'; } else { imagePreview.style.backgroundImage = 'none'; imagePreview.style.display = 'none'; } }; fileInput.addEventListener('change', (e) => { if (e.target.files && e.target.files[0]) { const formData = new FormData(); formData.append('file', e.target.files[0]); uploadBtn.innerHTML = ''; uploadBtn.disabled = true; fetch('/api/manage/media/upload', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success === 1 && data.file && data.file.url) { imageInput.value = data.file.url; updatePreview(); } else { alert('Upload failed: ' + (data.message || 'Unknown error')); } }) .catch(err => { console.error('Error uploading image', err); alert('Upload failed: ' + err.message); }) .finally(() => { uploadBtn.innerHTML = ' Upload'; uploadBtn.disabled = false; fileInput.value = ''; // Reset }); } }); appendDiv.appendChild(chooseBtn); appendDiv.appendChild(uploadBtn); inputRow.appendChild(imageInput); inputRow.appendChild(appendDiv); inputRow.appendChild(fileInput); // Image Preview Container const imagePreview = document.createElement('div'); imagePreview.style.width = '100%'; imagePreview.style.height = '180px'; imagePreview.style.backgroundColor = '#f8f9fc'; imagePreview.style.border = '1px dashed #d1d3e2'; imagePreview.style.borderRadius = '6px'; imagePreview.style.backgroundSize = 'cover'; imagePreview.style.backgroundPosition = 'center'; imagePreview.style.display = 'none'; imageInput.addEventListener('input', updatePreview); updatePreview(); imageGroup.appendChild(imageLabel); imageGroup.appendChild(inputRow); imageGroup.appendChild(imagePreview); const attrGroup = this._createInput('e.g. data-aos="fade-right" style="..."', item.attributes, 'attr', ' Step Custom Attributes'); itemBox.appendChild(dateGroup); itemBox.appendChild(descGroup); itemBox.appendChild(imageGroup); itemBox.appendChild(attrGroup); this.itemsContainer.appendChild(itemBox); } _createInput(placeholder, value, type, labelHtml) { const wrapper = document.createElement('div'); wrapper.className = 'form-group mb-2'; if (labelHtml) { const label = document.createElement('label'); label.className = 'small font-weight-bold text-secondary mb-1 d-block'; label.innerHTML = labelHtml; wrapper.appendChild(label); } const inp = document.createElement('input'); inp.type = 'text'; inp.className = 'form-control form-control-sm ce-timeline-input-' + type; inp.placeholder = placeholder; inp.value = value || ''; if (this.readOnly) inp.disabled = true; wrapper.appendChild(inp); return wrapper; } _createTextarea(placeholder, value, type, labelHtml) { const wrapper = document.createElement('div'); wrapper.className = 'form-group mb-2'; if (labelHtml) { const label = document.createElement('label'); label.className = 'small font-weight-bold text-secondary mb-1 d-block'; label.innerHTML = labelHtml; wrapper.appendChild(label); } const inp = document.createElement('textarea'); inp.className = 'form-control form-control-sm ce-timeline-input-' + type; inp.placeholder = placeholder; inp.value = value || ''; inp.style.minHeight = '70px'; if (this.readOnly) inp.disabled = true; wrapper.appendChild(inp); return wrapper; } save(blockContent) { const itemBoxes = this.itemsContainer.querySelectorAll('.timeline-item-edit-box'); const items = []; itemBoxes.forEach(box => { const date = box.querySelector('.ce-timeline-input-date').value.trim(); const desc = box.querySelector('.ce-timeline-input-desc').value.trim(); const img = box.querySelector('.ce-timeline-input-image').value.trim(); const attrEl = box.querySelector('.ce-timeline-input-attr'); const attr = attrEl ? attrEl.value.trim() : ''; const imgStyle = (box.querySelector('.ce-timeline-input-imgstyle') || {}).value || ''; const imgClass = (box.querySelector('.ce-timeline-input-imgclass') || {}).value || ''; const imgAlt = (box.querySelector('.ce-timeline-input-imgalt') || {}).value || ''; const imgAttrs = (box.querySelector('.ce-timeline-input-imgattrs') || {}).value || ''; const imgAspectRatio = (box.querySelector('.ce-timeline-input-imgar') || {}).value || ''; if (date || desc || img || attr) { items.push({ date: date, description: desc, imageUrl: img, attributes: attr, imageStyle: imgStyle, imageClass: imgClass, imageAlt: imgAlt, imageAttrs: imgAttrs, imageAspectRatio: imgAspectRatio }); } }); const globalAttrEl = this.wrapper.querySelector('.ce-timeline-input-global-attr'); return { globalAttributes: globalAttrEl ? globalAttrEl.value.trim() : (this.data.globalAttributes || ''), items: items }; } } // Register the plugin globally window.SISEditorPlugins = window.SISEditorPlugins || {}; window.SISEditorPlugins['timeline'] = { class: SISTimelineTool }; // openMediaPickerModal — legacy stub, replaced by SISMediaPicker function openMediaPickerModal(onSelectCallback) { if (window.SISMediaPicker) { SISMediaPicker.open(function(url) { onSelectCallback(url); }); } else { var url = prompt('Enter image URL:'); if (url) onSelectCallback(url); } }